package com.google.code.yargon.level;

import java.awt.geom.Point2D;
import java.util.BitSet;

import org.apache.log4j.Logger;

import com.google.code.yargon.ui.RenderEngine;
import com.google.code.yargon.ui.lighting.Intersection;
import com.google.code.yargon.ui.lighting.Ray;
import com.google.code.yargon.util.LogUtil;
import com.google.code.yargon.util.YargonUtil;

public class LevelState {

	// Possible states for each level block
	private static final int STATE_KNOWN = 0;
	private static final int STATE_VISIBLE = 1;
	
	private int w;
	private int h;
	private BitSet[][] levelState;
	private float[][] lightLevel; // Scale 0 .. 1.0f
	
	private Logger log = LogUtil.getInstance();
	
	public LevelState(LevelArea level) {	
		this.w = level.getWidth();
		this.h = level.getHeight();
		this.levelState = new BitSet[w][h];
		this.lightLevel = new float[w][h];
		init();
	}
	
	/**
	 * Hide and mark all areas unknown
	 */
	public void init() {
		for(int y = 0; y < h; y++) {
			for(int x = 0; x < w; x++) {
				levelState[x][y] = new BitSet();
				levelState[x][y].clear();
				lightLevel[x][y] = 0;
			}
		}
	}
	
	/**
	 * Only hide visibility
	 * 
	 */
	public void hide() {
		for(int y = 0; y < h; y++) {
			for(int x = 0; x < w; x++) {
				levelState[x][y].clear(STATE_VISIBLE);
				lightLevel[x][y] = 0;
			}
		}
	}

	/**
	 * Show all level
	 * 
	 */
	public void show() {
		for(int y = 0; y < h; y++) {
			for(int x = 0; x < w; x++) {
				levelState[x][y].set(STATE_KNOWN);
				levelState[x][y].set(STATE_VISIBLE);
				lightLevel[x][y] = 1.0f;
			}
		}
	}
	
	/**
	 * Is the location known
	 * 
	 * @param x
	 * @param y
	 * @return
	 */
	public boolean isKnown(int x, int y) {
		try {
			return this.levelState[x][y].get(STATE_KNOWN);
		}
		
		// In case of out of bounds
		catch(Exception e) {
			return false;
		}
	}
	
	/**
	 * Is the location visible
	 * 
	 * @param x
	 * @param y
	 * @return
	 */
	public boolean isVisible(int x, int y) {
		try {
			return this.levelState[x][y].get(STATE_VISIBLE);
		} 
		
		// In case of out of bounds
		catch(Exception e) {
			return false;
		}
	}
	
	/**
	 * Set known location
	 * 
	 * @param x
	 * @param y
	 * @param k
	 */
	public void setKnown(int x, int y, boolean k) {
		try {
			if(k == false) {
				this.levelState[x][y].clear(STATE_KNOWN);
			} 
			else {
				this.levelState[x][y].set(STATE_KNOWN);
			}
		}
		
		catch(Exception e) {
			// Ignore out of bounds
		}
	}
	
	/**
	 * Set visible location
	 * 
	 * @param x
	 * @param y
	 * @param v
	 */
	public void setVisible(int x, int y, boolean v) {
		try {
			if(v == false) {
				this.levelState[x][y].clear(STATE_VISIBLE);
			}
			else {
				this.levelState[x][y].set(STATE_VISIBLE);
			}
		}
		
		catch(Exception e) {
			// Ignore out of bounds
		}
	}
	
	/**
	 *  Set light level for this block
	 *  
	 * @param x
	 * @param y
	 * @param l
	 */
	public void setLightLevel(int x, int y, float l) {
		lightLevel[x][y] = l;
	}
	
	/** 
	 * Set the light level for this block
	 * 
	 * @param x
	 * @param y
	 * @return
	 */
	public float getLightLevel(int x, int y) {
		return lightLevel[x][y];
	}
	
	/**
	 * Update level visibility and known areas according to current line of sight
	 * 
	 */
	public void update(Location l, LevelArea level) {
		
		int nRays = 0;
		int nInts = 0;
		int visionRadius = 8;	// TODO: At the moment no effect - in future visionRadius should allow other light sources to be seen from further away then the player's light allows
		int lightRadius = 8;
		
		long st = YargonUtil.getTime();
		hide();
				
		Ray[] playerVisionRays = RenderEngine.getLightMap(visionRadius);
		
		// For each ray 
		for(Ray ray : playerVisionRays) {
			
			// Target block coordinates
			int tBx = ray.getBlockX() + l.getX();
			int tBy = ray.getBlockY() + l.getY();
		
			// If the target block is undefined, we can skip this ray right away
			if(level.isUndefined(tBx, tBy)) {
				continue;
			}
			
			// Also if the target has been marked visible (by another ray) already, skip this ray
			if(isVisible(tBx, tBy)) {
				continue;
			} 
										
			nRays++;
			Intersection pi = null; // Previous intersection
			
			// For each intersection
			Intersection[] intersections = ray.getIntersections();
			for(Intersection intersection : intersections) {
				nInts++;
				
				// Intersected block and its coordinates
				int iBx = intersection.getX() + l.getX();
				int iBy = intersection.getY() + l.getY();
				Block iB = level.get(iBx, iBy);
				
				// Previously intersected block and its coordinates
				int pBx = 0;
				int pBy = 0; 
				Block pB = null;
				if(null != pi) {
					pBx = pi.getX() + l.getX();
					pBy = pi.getY() + l.getY();
					pB = level.get(pBx, pBy);
				}
					
				// If the previously intersected block was a tunnel or undefined block and this one is a wall, give up. Walls cannot be seen from tunnels.
				if((null == pB || pB instanceof TunnelBlock || pB instanceof UndefinedBlock || pB instanceof DoorBlock) && level.isWall(iBx, iBy)) {
					break;
				} 
					
				// If the intersected block is a wall or undefined (which is considered a tunnel wall) or a closed door (but not broken): Mark it visible and give up with this ray
				else if(level.isWall(iBx, iBy) || level.isUndefined(iBx, iBy) || (level.isDoor(iBx, iBy) && iB != null && ! ((DoorBlock)iB).isOpen() && ((DoorBlock)iB).getDurability() != 0)) {
					setKnown(iBx, iBy, true);
					setVisible(iBx, iBy, true);
					setLightLevel(iBx, iBy, calculateLight(l, iBx, iBy, lightRadius));
					break;
				} 
				
				// Or else, it is visible, go on
				else {
					setKnown(iBx, iBy, true);
					setVisible(iBx, iBy, true);
					setLightLevel(iBx, iBy, calculateLight(l, iBx, iBy, lightRadius));
					pi = intersection;
				}
			}
		}
		
		log.trace("Updating level state took "+(YargonUtil.getTime() - st)+" ms. Considered "+nRays+" rays and total of "+nInts+" intersections");
	}
	
	private float calculateLight(Location l, int x, int y, int lightRadius) {
		Point2D p0 = new Point2D.Float(l.getX(), l.getY());
		Point2D p1 = new Point2D.Float(x, y);
		
		double dist = p0.distance(p1);
		if(dist <= lightRadius) {
			return (float) (1 - (dist / (2 * lightRadius)));
		} 
		
		return 0.3f;
	}
}



